﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using YAFTS.Contracts;
using YAFTS.Contracts.Model;
using YAFTS.Utils;

namespace YAFTS.Core
{
	public class DefaultSearchEngine : ISearchEngine
	{
		private static readonly DefaultSearchEngine _instance;
		private readonly IConfiguration _configuration;
		private readonly IIndexStorage _indexStorage;
		private readonly ConcurrentBag<IIOObserver> _observers = new ConcurrentBag<IIOObserver>();
		private readonly ConcurrentBag<IWordsReader> _wordsReaders = new ConcurrentBag<IWordsReader>();
		private bool _isDisposed;
		public event EventHandler<ProccessFileExceptionArgs> ProccessError;

		static DefaultSearchEngine()
		{
			var configuration = new Configuration();
			_instance = new DefaultSearchEngine(configuration, new IndexStorage());
			_instance.AddWordsReader(new SimpleWordsReader(configuration));
			_instance.AddIOObserver(new FileSystemIOObserver(configuration));
		}

		public DefaultSearchEngine([NotNull] IConfiguration configuration,
		                           [NotNull] IIndexStorage indexStorage)
		{
			if (configuration == null)
			{
				throw new ArgumentNullException("configuration");
			}
			if (indexStorage == null)
			{
				throw new ArgumentNullException("indexStorage");
			}

			_configuration = configuration;
			_indexStorage = indexStorage;
		}

		public static ISearchEngine Instance
		{
			get { return _instance; }
		}


		public void AddObservedDirectory(string path, string fileMask = null, bool reqursive = true)
		{
			if (path == null)
			{
				throw new ArgumentNullException("path");
			}

			GetObserver(path).AddFolderToWatch(path, fileMask, reqursive);
		}

		public void AddObservedFile(string path)
		{
			if (path == null)
			{
				throw new ArgumentNullException("path");
			}

			GetObserver(path).AddFileToWatch(path);
		}

		public void AddIOObserver(IIOObserver observer)
		{
			if (observer == null)
			{
				throw new ArgumentNullException("observer");
			}

			_observers.Add(observer);
			observer.FileContentChange -= FileContentChange;
			observer.FileContentChange += FileContentChange;
			observer.FileRenamed -= ObserverOnFileRenamed;
			observer.FileRenamed += ObserverOnFileRenamed;
		}

		public void AddWordsReader(IWordsReader reader)
		{
			_wordsReaders.Add(reader);
		}

		private void ObserverOnFileRenamed(object sender, RenamedEventArgs renamedEventArgs)
		{
			_indexStorage.RenameFile(renamedEventArgs.OldFullPath, renamedEventArgs.FullPath);
		}

		public IEnumerable<string> ExecuteQuery(params string[] words)
		{
			return ExecuteQuery(SearchOptions.AnyWords, words);
		}

		public IEnumerable<string> ExecuteQuery(SearchOptions searchOptions, params string[] words)
		{
			switch (searchOptions)
			{
				case SearchOptions.AnyWords:
					return words.SelectMany(_indexStorage.GetWordFiles).Distinct().Select(x => x.Name).ToArray();
				case SearchOptions.AllWords:
					return words.Select(_indexStorage.GetWordFiles).Aggregate((a, b) => a.Intersect(b)).Select(x => x.Name);
				default:
					throw new ArgumentOutOfRangeException("searchOptions");
			}
		}

		public IConfiguration Configuration
		{
			get { return _configuration; }
		}

		public void Dispose()
		{
			if (_isDisposed)
			{
				return;
			}

			_indexStorage.Clear();
			_isDisposed = true;
			GC.SuppressFinalize(this);
		}

		private IIOObserver GetObserver(string path)
		{
			var observer = _observers.FirstOrDefault(x => x.CanObverve(path));
			if (observer == null)
			{
				throw new ArgumentException(string.Format("This path type is not supported. Path '{0}'", path), "path");
			}
			return observer;
		}
		private IWordsReader GetWordsReader(string path)
		{
			var reader = _wordsReaders.FirstOrDefault(x => x.SupportPath(path));
			if (reader == null)
			{
				throw new ArgumentException(string.Format("This path type is not supported. Path '{0}'", path), "path");
			}
			return reader;
		}

		private void OnProccessError(ProccessFileExceptionArgs e)
		{
			var handler = ProccessError;
			if (handler != null)
			{
				handler(this, e);
			}
		}

		private void ProcessFile(TextReader reader, string fileName)
		{
			var wordsInIndex = new HashSet<IWord>(_indexStorage.GetFileWords(fileName));
			var wordsInFile = GetWordsReader(fileName).Read(reader);

			_indexStorage.DeleteFromIndex(wordsInIndex.Except(wordsInFile), fileName);
			if (wordsInFile.Count > 0)
			{
				_indexStorage.AddToIndex(wordsInFile.Except(wordsInIndex), fileName);
			}
		}

		private void FileContentChange(object sender, FileContentChangedArgs e)
		{
			var task = new Task<TextReader>(e.GetContenStream);
			task.ContinueWith(t => ProcessFile(t.Result, e.FileName))
				.ContinueWith(t => OnProccessError(new ProccessFileExceptionArgs(e.FileName, t.Exception.With(x=>x.InnerException, t.Exception))),
				              TaskContinuationOptions.OnlyOnFaulted);

			task.Start();
		}

		~DefaultSearchEngine()
		{
			Dispose();
		}
	}
}